10. 스테이트 패턴

내부 상태를 바꿈으로써 객체에서 행동을 바꾸는 것을 도와준다.

  1. 10. 스테이트 패턴
    1. 주식회사 왕뽑기
    2. 패턴 적용 전
    3. 스트이트 패턴 적용 후
    4. 스테이트 패턴의 정의
    5. Q & A
    6. 핵심 정리
    7. 문서에 대하여

주식회사 왕뽑기

패턴 적용 전

GumballMachine.java


public class GumballMachine {
	final static int SOLD_OUT = 0;
	final static int NO_QUARTER = 1;
	final static int HAS_QUARTER = 2;
	final static int SOLD = 3;
	
	int state = SOLD_OUT;
	int count = 0;
	
	public GumballMachine(int count) {
		this.count = count;
		if (count > 0) {
			state = NO_QUARTER;
		}
	}
	
	public void insertQuarter() {
		if (state == HAS_QUARTER) {
			System.out.println("동전은 한 개만 넣어주세요");
		} else if (state == NO_QUARTER) {
			System.out.println("동전을 넣으셨습니다.");
			state = HAS_QUARTER;
		} else if (state == SOLD_OUT) {
			System.out.println("매진되었습니다. 다음에 이용해주세요.");
		} else if (state == SOLD) {
			System.out.println("잠깐만 기다려 주세요. 알맹이가 나가고 있습니다.");
		}
	}
	
	/**
	 * 동전 반환
	 */
	public void ejectQuarter() {
		if (state == HAS_QUARTER) {
			System.out.println("동전이 반환됩니다.");
			state = NO_QUARTER;
		} else if (state == NO_QUARTER) {
			System.out.println("동전을 넣어주세요.");
		} else if (state == SOLD) {
			System.out.println("이미 알맹이를 뽑으셨습니다.");
		} else if (state == SOLD_OUT) {
			System.out.println("동전을 넣지 않으셨습니다. 동전이 반환되지 않습니다.");
		}
	}
	
	public void turnCrank() {
		if (state == SOLD) {
			System.out.println("손잡이는 한 번만 돌려주세요.");
		} else if (state == NO_QUARTER) {
			System.out.println("동전을 넣어주세요.");
		} else if (state == SOLD_OUT) {
			System.out.println("매진되었습니다.");
		} else if (state == HAS_QUARTER) {
			System.out.println("손잡이를 돌리셨습니다.");
			state = SOLD;
			dispense();
		}
	}

	private void dispense() {
		if (state == SOLD) {
			System.out.println("알맹이가 나가고 있습니다.");
			count -= 1;
			if (count == 0) {
				System.out.println("더 이상 알맹이가 없습니다.");
				state = SOLD_OUT;
			} else {
				state = NO_QUARTER;
			}
		} else if (state == NO_QUARTER) {
			System.out.println("동전을 넣어주세요.");
		} else if (state == SOLD_OUT) {
			System.out.println("매진입니다.");
		} else if (state == HAS_QUARTER) {
			System.out.println("알맹이가 나갈 수 없습니다.");
		}
	}
	
	public String toString() {
		String stateStr = null;
		if (state == SOLD_OUT)
			stateStr = "알맹이 매진";
		else if (state == NO_QUARTER)
			stateStr = "동전없음(투입 대기중)";
		else if (state == HAS_QUARTER)
			stateStr = "동전있음";
		else if (state == SOLD) {
			stateStr = "알맹이판매";
		}
		return "남은 개수: "+ count + "\n기계상태: " + stateStr;
	}
}

GumballMachineTest.java


import org.junit.Test;

public class GumballMachineTest {
	@Test
	public void 테스트() {
		GumballMachine gumballMachine = new GumballMachine(5);
		System.out.println(gumballMachine);
		
		gumballMachine.insertQuarter();
		gumballMachine.turnCrank();
		
		System.out.println(gumballMachine);
		
		gumballMachine.insertQuarter();
		gumballMachine.ejectQuarter();
		gumballMachine.turnCrank();
		
		System.out.println(gumballMachine);
		
		gumballMachine.insertQuarter();
		gumballMachine.turnCrank();
		gumballMachine.insertQuarter();
		gumballMachine.turnCrank();
		gumballMachine.ejectQuarter();
		
		System.out.println(gumballMachine);
		
		gumballMachine.insertQuarter();
		gumballMachine.insertQuarter();
		gumballMachine.turnCrank();
		gumballMachine.insertQuarter();
		gumballMachine.turnCrank();
		gumballMachine.insertQuarter();
		gumballMachine.turnCrank();
		
		System.out.println(gumballMachine);
	}
}

스트이트 패턴 적용 후

  1. 뽑기 기계와 관련된 모든 행동에 대한 메소드가 들어있는 State 인터페이스 정의한다.
  2. 기계의 모든 상태에 대해서 상태 클래스를 구현한다.
  3. 마지막으로 조건문 코드를 없애고 상태 클래스에 모든 작업을 위임한다.

GumballMachine.java


public class GumballMachine {
	State soldOutState;
	State noQuarterState;
	State hasQuarterState;
	State soldState;
	
	State state = soldOutState;
	int count = 0;
	
	public GumballMachine(int numberGumballs) {
		soldOutState = new SoldOutState(this);
		noQuarterState = new NoQuarterState(this);
		hasQuarterState = new HasQuarterState(this);
		soldState = new SoldState(this);
		
		this.count = numberGumballs;
		if (numberGumballs > 0) {
			state = noQuarterState;
		}
	}

	public void insertQuarter() {
		state.insertQuarter();
	}
	
	public void ejectQuarter() {
		state.ejectQuarter();
	}
	
	public void turnCrank() {
		state.turnCrank();
		state.dispense();
	}
	
	public void setState(State state) {
		this.state = state;
	}
	
	public void releaseBall() {
		System.out.println("A gumball comes rolling out the slot...");
		if (count != 0) {
			count -= 1;
		}
	}
	
	public void refill(int count) {
		this.count = count;
		state = noQuarterState;
	}
	
	public State getSoldOutState() {
		return soldOutState;
	}

	public State getNoQuarterState() {
		return noQuarterState;
	}

	public State getHasQuarterState() {
		return hasQuarterState;
	}

	public State getSoldState() {
		return soldState;
	}

	public State getState() {
		return state;
	}

	public int getCount() {
		return count;
	}

	public String toString() {
		StringBuffer sb = new StringBuffer();
		sb.append("\nMighty Gumball, Inc.");
		sb.append("\nJava-enabled Standing Gumball Model #2004");
		sb.append("\nInventory: " + count + " gumball");
		if (count != 1) {
			sb.append("s");
		}
		sb.append("\nMachine is " + state + "\n");
		return sb.toString();
	}
}

State.java


public interface State {
	void insertQuarter();
	void ejectQuarter();
	void turnCrank();
	void dispense();
}

SoldOutState.java


public class SoldOutState implements State {
	private GumballMachine gumballMachine;
	
	public SoldOutState(GumballMachine gumballMachine) {
		this.gumballMachine = gumballMachine;
	}

	public void insertQuarter() {
		System.out.println("동전을 넣을 수 없습니다. 매진입니다.");
	}
	
	public void ejectQuarter() {
		System.out.println("알맹이를 꺼낼 수 없습니다.");
	}

	public void turnCrank() {
		System.out.println("손잡이를 돌리셨습니다. 알맹이가 매진되었습니다.");
	}
	
	public void dispense() {
		System.out.println("알맹이가 매진되었습니다.");
	}
	
	public String toString() {
		return "알맹이 매진";
	}
}

NoQuarterState.java


public class NoQuarterState implements State {
	GumballMachine gumballMachine;
	
	public NoQuarterState(GumballMachine gumballMachine) {
		this.gumballMachine = gumballMachine;
	}

	public void insertQuarter() {
		System.out.println("동전을 넣으셨습니다.");
		gumballMachine.setState(gumballMachine.getHasQuarterState());
	}

	public void ejectQuarter() {
		System.out.println("동전을 넣어주세요.");
	}
	
	public void turnCrank() {
		System.out.println("동전을 넣어주세요.");
	}
	
	public void dispense() {
		System.out.println("동전을 넣어주세요.");
	}

	public String toString() {
		return "동전없음";
	}
}

HasQuarterState.java


public class HasQuarterState implements State {
	private GumballMachine gumballMachine;
	
	public HasQuarterState(GumballMachine gumballMachine) {
		this.gumballMachine = gumballMachine;
	}
	
	public void insertQuarter() {
		System.out.println("동전은 한 개만 넣어주세요.");
	}

	public void ejectQuarter() {
		System.out.println("동전이 반환됩니다.");
		gumballMachine.setState(gumballMachine.getNoQuarterState());
	}

	public void turnCrank() {
		System.out.println("손잡이를 돌리셨습니다.");
		gumballMachine.setState(gumballMachine.getSoldState());
	}
	
	public void dispense() {
		System.out.println("알맹이가 나갈 수 없습니다.");
	}
	
	public String toString() {
		return "동전있음";
	}
}

SoldState.java


package net.dblab.hfdp.state.after;

public class SoldState implements State {
	private GumballMachine gumballMachine;
	
	public SoldState(GumballMachine gumballMachine) {
		this.gumballMachine = gumballMachine;
	}

	public void insertQuarter() {
		System.out.println("잠깐만 기다려주세요. 알맹이가 나가고 있습니다.");
	}
	
	public void ejectQuarter() {
		System.out.println("이미 알맹이를 뽑으셨습니다.");
	}

	public void turnCrank() {
		System.out.println("손잡이는 한 번만 돌려주세요.");
	}
	
	public void dispense() {
		gumballMachine.releaseBall();
		if (gumballMachine.getCount() > 0) {
			gumballMachine.setState(gumballMachine.getNoQuarterState());
		} else {
			System.out.println("Oops!! out of gumballs!!");
			gumballMachine.setState(gumballMachine.getSoldOutState());
		}
	}
	
	public String toString() {
		return "알맹이 나가는 중";
	}
}

스테이트 패턴의 정의

객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있다. 마치 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있다.

스트래티지 패턴과 비교

(상속이 아닌) Composition을 통해 행동을 정의하는 객체를 만든다.

Q & A

핵심 정리

  • 스테이트 패턴을 이용하면 내부 상태를 바탕으로 서로 다른 행동을 할 수 있다.
  • 각 상태를 클래스를 이용하여 표현
  • Context 객체에서는 현재 상태에게 행동을 위임한다.
  • 각 상태를 클래스로 캡슐화함으로써 나중에 변경해야하는 내용을 국지화시킬 수 있다.
  • 스테이트 패턴과 스트래티지 패턴의 클래스 다이어그램은 똑같다.
  • 스트래티지 패턴에서는 행동 또는 알고리즘을 Context클래스를 만들 때 설정한다.
  • 상태 전환은 State 클래스에 의해 제어할 수 있고 Context클래스에 의해서 제어할 수도 있다.
  • 클래스 개수가 늘어나는 단점이 있다.
  • State 클래스를 여러 Context객체의 인스턴스에서 공유하도록 디자인 가능하다.

문서에 대하여